﻿using System;
using System.Linq;
using System.Reflection;

using Windows.UI.Xaml;

namespace Uhimania.Libraries.Windows.Interactivity
{
    #region EventTrigger
    /// <summary>
    /// ソース上の指定イベントをリッスンし、そのイベントが発生すると
    /// 起動される <see cref="TriggerBase"/>。
    /// </summary>
    public class EventTrigger : TriggerBase
    {
        #region EventName

        /// <summary>
        /// <see cref="EventName"/> を識別する依存関係プロパティ。
        /// </summary>
        public static readonly DependencyProperty EventNameProperty =
            DependencyProperty.Register(
                "EventName",
                typeof(string),
                typeof(EventTrigger),
                new PropertyMetadata(null, EventNameChanged)
            );

        /// <summary>
        /// リッスンするイベントの名前を取得、または設定する。
        /// </summary>
        public string EventName
        {
            get { return this.GetValue(EventNameProperty) as string; }
            set { this.SetValue(EventNameProperty, value); }
        }

        /// <summary>
        /// <see cref="EventName"/> が変更されたときに呼び出される。
        /// </summary>
        /// <param name="obj">イベントソース。</param>
        /// <param name="e">イベントデータ。</param>
        private static void EventNameChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var trigger = obj as EventTrigger;
            var source = trigger.SourceObjectCore;
            trigger.DetachEvent(source, e.OldValue as string);
            trigger.AttachEvent(source, e.NewValue as string);
        }

        #endregion

        #region SourceObject

        /// <summary>
        /// <see cref="SourceObject"/> を識別する依存関係プロパティ。
        /// </summary>
        public static readonly DependencyProperty SourceObjectProperty =
            DependencyProperty.Register(
                "SourceObject",
                typeof(object),
                typeof(EventTrigger),
                new PropertyMetadata(null, SourceObjectChanged)
            );

        /// <summary>
        /// ターゲットオブジェクトを取得、または設定する。
        /// </summary>
        public object SourceObject
        {
            get { return this.GetValue(SourceObjectProperty); }
            set { this.SetValue(SourceObjectProperty, value); }
        }

        /// <summary>
        /// <see cref="SourceObject"/> が変更されたときに呼び出される。
        /// </summary>
        /// <param name="obj">イベントソース。</param>
        /// <param name="e">イベントデータ。</param>
        private static void SourceObjectChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var trigger = obj as EventTrigger;
            trigger.SourceObjectCore = e.NewValue;
        }

        /// <summary>
        /// イベントのターゲットオブジェクト。
        /// </summary>
        private object sourceObjectCore;

        /// <summary>
        /// ターゲットオブジェクトを取得、または設定する。
        /// </summary>
        /// <remarks>
        /// イベントのターゲットオブジェクトを取得、または設定する。<br />
        /// Binding が評価される前に <see cref="SourceObject"/> に <see cref="AttachedObjectBase.AssociatedObject"/>
        /// が設定されると Binding が働かないため、このプロパティを用いる。
        /// </remarks>
        private object SourceObjectCore
        {
            get { return this.sourceObjectCore; }
            set
            {
                if (object.Equals(this.sourceObjectCore, value))
                    return;

                var oldValue = this.sourceObjectCore;

                this.sourceObjectCore = value;

                this.OnSourceObjectCoreChanged(oldValue, value);
            }
        }

        /// <summary>
        /// <see cref="SourceObjectCore"/> が変更されたときに呼び出される。
        /// </summary>
        /// <param name="oldValue">変更前のオブジェクト。</param>
        /// <param name="newValue">変更後のオブジェクト。</param>
        private void OnSourceObjectCoreChanged(object oldValue, object newValue)
        {
            this.DetachEvent(oldValue, this.EventName);
            this.AttachEvent(newValue, this.EventName);
        }

        #endregion

        #region OnAssociatedObjectChanged
        /// <summary>
        /// <see cref="AttachedObjectBase.AssociatedObject"/> が変更されたときに呼び出される。
        /// </summary>
        /// <param name="oldValue">変更前の値。</param>
        /// <param name="newValue">変更後の値。</param>
        protected override void OnAssociatedObjectChanged(DependencyObject oldValue, DependencyObject newValue)
        {
            base.OnAssociatedObjectChanged(oldValue, newValue);

            if (object.Equals(this.SourceObjectCore, oldValue))
                this.SourceObjectCore = null;

            if (this.SourceObjectCore == null)
                this.SourceObjectCore = newValue;
        }
        #endregion

        #region AttachEvent / DetachEvent

        /// <summary>
        /// イベントにアタッチされたデリゲート。
        /// </summary>
        private Delegate eventHandler;

        /// <summary>
        /// イベントをアタッチする。
        /// </summary>
        /// <param name="source">イベントのターゲットオブジェクト。</param>
        /// <param name="eventName">リッスンするイベント名。</param>
        private void AttachEvent(object source, string eventName)
        {
            if (source == null || string.IsNullOrWhiteSpace(eventName))
                return;

            var eventInfo = source.GetType().GetRuntimeEvents().Where(e => e.Name == eventName).FirstOrDefault();
            if (eventInfo == null)
                throw new ArgumentException(string.Format("event <{0}> is not found in <{1}>", eventName, source.GetType()));

            var handler = new Action<object, object>((obj, e) => this.InvokeActions(e));
            var methodInfo = handler.GetType().GetRuntimeMethods().Where(m => m.Name == "Invoke").FirstOrDefault();
            this.eventHandler = methodInfo.CreateDelegate(eventInfo.EventHandlerType, handler);

            eventInfo.AddMethod.Invoke(source, new[] { this.eventHandler });
        }

        /// <summary>
        /// イベントをデタッチする。
        /// </summary>
        /// <param name="source">イベントのターゲットオブジェクト。</param>
        /// <param name="eventName">リッスンするイベント名。</param>
        private void DetachEvent(object source, string eventName)
        {
            if (source == null || string.IsNullOrWhiteSpace(eventName))
                return;

            var eventInfo = source.GetType().GetRuntimeEvents().Where(e => e.Name == eventName).FirstOrDefault();
            if (eventInfo == null)
                throw new ArgumentException(string.Format("event <{0}> is not found in <{1}>", eventName, source.GetType()));

            if (this.eventHandler == null)
                return;

            eventInfo.RemoveMethod.Invoke(source, new[] { this.eventHandler });
        }

        #endregion
    }
    #endregion
}
